using System;
using System.Collections.Generic;
using System.Text;

namespace GPSCore
{
    /// <summary>
    /// Rappresent a Distance betweend two Points
    /// 
    /// By Default the Distance Object has an absolute value of 0 meters direction North
    /// </summary>
    public class Distance
    {
        #region members

        /// <summary>
        /// distance value in meters
        /// </summary>
        private double _absoluteValue;

        /// <summary>
        /// the footprint on earth distance
        /// </summary>
        private double _onEarthDistance;

        /// <summary>
        /// directions 
        /// </summary>
        private Direction _direction;

        /// <summary>
        /// the height distance 
        /// </summary>
        private double _height;

        #endregion

        #region constructors

        /// <summary> 
        /// Basic Constructor of a "distance object" 
        /// </summary>
        public Distance()
        {
            _absoluteValue = 0;
            _onEarthDistance = 0;
            _direction = new Direction();
        }
        
        /// <summary>
        /// Build a Distance Value from Absolute Value (by default the direction is North)
        /// </summary>
        /// <param name="AbsoluteValue"></param>
        public Distance(double AbsoluteValue)
        {
            _absoluteValue = AbsoluteValue;
            _onEarthDistance = _absoluteValue;
            _direction = new Direction();

            _expandHeight();
        }

        /// <summary>
        /// Build a Distance Value from Absolute Value and Direction
        /// </summary>
        /// <param name="AbsoluteValue"></param>
        /// <param name="Direction"></param>
        public Distance(double AbsoluteValue, Direction Direction)
        {
            _absoluteValue = AbsoluteValue;
            _direction = Direction;

            _expandOnEarthDistance();
            _expandHeight();
        }

        /// <summary>
        /// Build a Distance Value from Absolute Value and Direction (from Azimuth and Elevation)
        /// </summary>
        /// <param name="AbsoluteValue"></param>
        /// <param name="Azimuth"></param>
        /// <param name="Elevation"></param>
        public Distance(double AbsoluteValue, Azimuth Azimuth, Elevation Elevation)
        {
            _absoluteValue = AbsoluteValue;
            _direction = new Direction(Azimuth, Elevation);
            
            _expandOnEarthDistance();
            _expandHeight();
        }

        /// <summary>
        /// Build a Distance Value from Absolute Value and Direction (just Azimuth, Elevation = 0)
        /// </summary>
        /// <param name="AbsoluteValue"></param>
        /// <param name="Azimuth"></param>
        public Distance(double AbsoluteValue, Azimuth Azimuth)
        {
            _absoluteValue = AbsoluteValue;
            _direction = new Direction(Azimuth);
            _onEarthDistance = _absoluteValue;
            
            _expandHeight();
        }

        /// <summary>
        /// Build a Distance starting from P1 reaching P2 
        /// </summary>
        /// <param name="P1"></param>
        /// <param name="P2"></param>
        public Distance(Position P1, Position P2)
        {
            calculateDistance(P1, P2);
            _direction = new Direction(P1, P2);

            _expandHeight();
        }

        #endregion

        #region properties

        /// <summary>
        /// Altitude Difference
        /// </summary>
        public double Height
        {
            get { return _height; }
        }

        /// <summary>
        /// Direction evaluated by distance mesure
        /// </summary>
        public Direction Direction
        {
            get { return _direction; }            
        }

        /// <summary>
        /// Absolute Value (Meters)
        /// </summary>
        public double AbsoluteValue
        {
            get { return _absoluteValue; }          
        }

        /// <summary>
        /// The On Earth Distance (do not take kare of heigh distance of points)
        /// </summary>
        public double OnEarthDistance
        {
            get { return _onEarthDistance; }
        }


        #endregion

        #region methods


        /// <summary>
        /// calculate the distance on earth
        /// </summary>
        private void _expandOnEarthDistance()
        {
            //calculate the exansion of module + direction into geometry
            double latD = _absoluteValue * Math.Cos(GPSConvertManager.DegreesToRadians(_direction.Azimuth.DecimalDegrees));
            double longD = _absoluteValue * Math.Sin(GPSConvertManager.DegreesToRadians(_direction.Azimuth.DecimalDegrees));

            _onEarthDistance = Math.Sqrt((latD * latD) + (longD * longD));
        }

        /// <summary>
        /// calculate the different in height
        /// </summary>
        private void _expandHeight()
        {
            _height =  GPSConvertManager.RoundDistance( _absoluteValue * Math.Sin(GPSConvertManager.DegreesToRadians(_direction.Elevation.DecimalDegrees)));        
        }

        /// <summary>
        /// evalue the distance between two points
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        private void calculateDistance(Position p1, Position p2)
        { 
            // Distance (basing on the great circle distance
            _onEarthDistance = GPSConvertManager.RoundDistance(GPSConvertManager.EarthRadiusKms * p1.GreatCircleDistanceTo(p2) * 1000.0); //* 1000 to convert in meters

            if (p1.Altitude.HasValue & p2.Altitude.HasValue)
            {
                // add the elevation difference to the calculation
                double elevationDifferenceMeters = p2.Altitude.Value - p1.Altitude.Value;

                _absoluteValue = GPSConvertManager.RoundDistance( Math.Sqrt(_onEarthDistance * _onEarthDistance + elevationDifferenceMeters * elevationDifferenceMeters) );
            }
            else
            {
                _absoluteValue = _onEarthDistance;

                //fire an event?  no elevation information to calculate the distance
                //so it can be handled by a custom function and calculated by an override way
            }
        }

        #endregion

        #region operators


        /// <summary>
        /// Sum two Distance Object (fully Azimuth and Elevation.
        /// </summary>
        /// <param name="Distance1"></param>
        /// <param name="Distance2"></param>
        /// <returns></returns>
        public static Distance operator +(Distance Distance1, Distance Distance2)
        {
            Distance temp = new Distance();
                                    
            //calculate the exansion of module + direction into geometry
            double latD1 = Distance1._absoluteValue * Math.Cos( GPSConvertManager.DegreesToRadians( Distance1.Direction.Azimuth.DecimalDegrees ) );
            double longD1 = Distance1._absoluteValue * Math.Sin( GPSConvertManager.DegreesToRadians( Distance1.Direction.Azimuth.DecimalDegrees ) );
            double elevD1 = Distance1._absoluteValue * Math.Sin(GPSConvertManager.DegreesToRadians(Distance1.Direction.Elevation.DecimalDegrees));
            
            double latD2 = Distance2._absoluteValue * Math.Cos( GPSConvertManager.DegreesToRadians( Distance1.Direction.Azimuth.DecimalDegrees ) );
            double longD2 = Distance2._absoluteValue * Math.Sin( GPSConvertManager.DegreesToRadians( Distance1.Direction.Azimuth.DecimalDegrees ) );
            double elevD2 = Distance2._absoluteValue * Math.Sin(GPSConvertManager.DegreesToRadians(Distance1.Direction.Elevation.DecimalDegrees));

            double latResult = latD1 + latD2;
            double longRestul = longD1 + longD2;
            double elevResult = elevD1 + elevD2;

            temp._onEarthDistance = Math.Sqrt((latResult * latResult) + (longRestul * longRestul) );
            temp._absoluteValue = Math.Sqrt( (temp._onEarthDistance * temp._onEarthDistance ) + (elevResult * elevResult));
            temp._direction = Distance1._direction + Distance2._direction;

            return temp;
        }


        /// <summary>
        /// Return Back a Velocity Object
        /// 
        /// Velocity = Distance / Time
        /// 
        /// </summary>
        /// <param name="Distance"></param>
        /// <param name="TimeInSeconds"></param>
        /// <returns></returns>
        public static Velocity operator /(Distance Distance, double TimeInSeconds)
        {
            Velocity v = new Velocity();

            v.Direction.Azimuth.DecimalDegrees = Distance.Direction.Azimuth.DecimalDegrees;
            v.Direction.Elevation.DecimalDegrees = Distance.Direction.Elevation.DecimalDegrees;
            v.Speed.Value = Distance.AbsoluteValue / TimeInSeconds;

            return v;
        }

        /// <summary>
        /// Subtract two Distance Object (fully Azimuth and Elevation.
        /// </summary>
        /// <param name="Distance"></param>
        /// <returns></returns>
        public static Distance operator -(Distance Distance)
        {
            Distance temp = new Distance();

            temp._absoluteValue = Distance._absoluteValue;
            temp._onEarthDistance = Distance._onEarthDistance;
            
            Azimuth newAzimuth = new Azimuth( Distance._direction.Azimuth.DecimalDegrees + 180.0 );
            Elevation newElevation = new Elevation( - Distance._direction.Elevation.DecimalDegrees );


            temp._direction = new Direction(newAzimuth, newElevation);

            return temp;
        }

        #endregion
    }
}
